﻿#region COPYRIGHT© 2005-2013 Phillip Clark. All rights reserved.

// For licensing information see License.txt (MIT style licensing).

#endregion

using System;
using System.Globalization;

namespace FlitBit.Parse
{
	/// <summary>
	/// Structure specifying a character match.
	/// </summary>
	public struct CharMatch : IEquatable<CharMatch>
	{
		static readonly int CHashCodeSeed = typeof(CharMatch).AssemblyQualifiedName.GetHashCode();

		readonly UnicodeCategory _category;
		readonly CharMatchKind _kind;
		readonly char _opposing;
		readonly char _primary;

		CharMatch(CharMatchKind kind, UnicodeCategory cat, char primary, char opposing)
		{
			_kind = kind;
			_category = cat;
			_primary = primary;
			_opposing = opposing;
		}

		/// <summary>
		/// Indicates whether the match is case-sensitive.
		/// </summary>
		public bool IsCaseSensitive { get { return (_kind & CharMatchKind.CaseInsensitive) != CharMatchKind.CaseInsensitive; } }

		/// <summary>
		/// Indicates whether the	match has a variable value.
		/// </summary>
		public bool IsVariable
		{
			get
			{
				// If the kind is Exact or CaseInsensitive then we're matching known characters
				// otherwise it's a variable match.
				return !(_kind == CharMatchKind.Exact
					|| (_kind & CharMatchKind.CaseInsensitive) == CharMatchKind.CaseInsensitive);
			}
		}

		/// <summary>
		/// Gets any static characters at the beginning of the match.
		/// </summary>
		public char[] SpecificChars
		{
			get
			{
				return (IsVariable)
					? new char[0] // no specific chars if variable
					: (IsCaseSensitive)
						? new[] {_primary}
						: new[] {_primary, _opposing};
			}
		}

		/// <summary>
		/// Determines if the instance is equal to another.
		/// </summary>
		/// <param name="obj"></param>
		/// <returns></returns>
		public override bool Equals(object obj)
		{
			return obj is CharMatch
				&& this.Equals((CharMatch) obj);
		}

		/// <summary>
		/// Gets the instance's hash code.
		/// </summary>
		/// <returns></returns>
		public override int GetHashCode()
		{
			const int prime = 999067; // a random prime
			var result = CHashCodeSeed * prime;
			result ^= (int) _kind * prime;
			result ^= (int) _category * prime;
			result ^= this._primary * prime;
			result ^= this._opposing * prime;
			return result;
		}

		/// <summary>
		/// Converts the match to its string representation.
		/// </summary>
		/// <returns></returns>
		public override string ToString()
		{
			switch (_kind)
			{
				case CharMatchKind.CaseInsensitive:
					return String.Concat("match character '", _primary.MakeHumanReadableWhitespace(), "' or '",
															_opposing.MakeHumanReadableWhitespace(), "'");
				case CharMatchKind.UnicodeCategory:
					return String.Concat("match character category ", _category);
				case CharMatchKind.Not:
					return String.Concat("match character if not '", _primary.MakeHumanReadableWhitespace(), "'");
				case CharMatchKind.NotCaseInsensitive:
					return String.Concat("match character if not '", _primary.MakeHumanReadableWhitespace(), "' or '",
															_opposing.MakeHumanReadableWhitespace(), "'");
				case CharMatchKind.NotUnicodeCategory:
					return String.Concat("match character category if not ", _category);
				default:
					//case CharMatchKind.NewLine:
					//case CharMatchKind.Exact:
					return String.Concat("match character '", _primary.MakeHumanReadableWhitespace(), "'");
			}
		}

		/// <summary>
		///   Determines if the input cursor is at a matching character.
		/// </summary>
		/// <param name="input">input</param>
		/// <param name="cursor">the cursor</param>
		/// <returns>
		///   <em>true</em> if the character matches the input at the cursor; otherwise <em>false</em>
		/// </returns>
		public bool At(char[] input, Cursor cursor)
		{
			if (cursor >= input.Length)
			{
				return false;
			}
			var c = input[cursor];
			switch (_kind)
			{
				case CharMatchKind.CaseInsensitive:
					return _primary == c || _opposing == c;
				case CharMatchKind.UnicodeCategory:
					return char.GetUnicodeCategory(c) == _category;
				case CharMatchKind.Not:
					return _primary != c;
				case CharMatchKind.Range:
					return c >= _primary && c <= _opposing;
				case CharMatchKind.NotRange:
					return c < _primary || c > _opposing;
				case CharMatchKind.NotCaseInsensitive:
					return _primary != c && _opposing != c;
				case CharMatchKind.NotUnicodeCategory:
					return char.GetUnicodeCategory(c) != _category;
				case CharMatchKind.Exact:
					return _primary == c;
			}
			return false;
		}

		/// <summary>
		///   Determines if the input cursor is at a matching character.
		/// </summary>
		/// <param name="input">input</param>
		/// <param name="cursor">the cursor</param>
		/// <returns>
		///   <em>true</em> if the character matches the input at the cursor; otherwise <em>false</em>
		/// </returns>
		public bool At(string input, Cursor cursor)
		{
			if (cursor >= input.Length)
			{
				return false;
			}
			var c = input[cursor];
			switch (_kind)
			{
				case CharMatchKind.CaseInsensitive:
					return _primary == c || _opposing == c;
				case CharMatchKind.UnicodeCategory:
					return char.GetUnicodeCategory(c) == _category;
				case CharMatchKind.Not:
					return _primary != c;
				case CharMatchKind.Range:
					return c >= _primary && c <= _opposing;
				case CharMatchKind.NotRange:
					return c < _primary || c > _opposing;
				case CharMatchKind.NotCaseInsensitive:
					return _primary != c && _opposing != c;
				case CharMatchKind.NotUnicodeCategory:
					return char.GetUnicodeCategory(c) != _category;
				case CharMatchKind.Exact:
					return _primary == c;
			}
			return false;
		}

		/// <summary>
		///   Tries to match the input at the cursor.
		/// </summary>
		/// <param name="input">input</param>
		/// <param name="cursor">the cursor</param>
		/// <returns>
		///   <em>true</em> if the character matches the input at the cursor; otherwise <em>false</em>
		/// </returns>
		public bool TryAt(char[] input, ref Cursor cursor)
		{
			if (At(input, cursor))
			{
				cursor++;
				return true;
			}
			return false;
		}

		/// <summary>
		///   Tries to match the input at the cursor.
		/// </summary>
		/// <param name="input">input</param>
		/// <param name="cursor">the cursor</param>
		/// <returns>
		///   <em>true</em> if the character matches the input at the cursor; otherwise <em>false</em>
		/// </returns>
		public bool TryAt(string input, ref Cursor cursor)
		{
			if (At(input, cursor))
			{
				cursor++;
				return true;
			}
			return false;
		}

		/// <summary>
		/// Equality operator; determines if two values are equal.
		/// </summary>
		/// <param name="lhs"></param>
		/// <param name="rhs"></param>
		/// <returns></returns>
		public static bool operator ==(CharMatch lhs, CharMatch rhs)
		{
			return lhs.Equals(rhs);
		}

		/// <summary>
		/// Inequality operator; determines if two values are inequal.
		/// </summary>
		/// <param name="lhs"></param>
		/// <param name="rhs"></param>
		/// <returns></returns>
		public static bool operator !=(CharMatch lhs, CharMatch rhs)
		{
			return !lhs.Equals(rhs);
		}

		#region IEquatable<CharMatch> Members

		/// <summary>
		/// Indicates whether the current object is equal to another object of the same type.
		/// </summary>
		/// <returns>
		/// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
		/// </returns>
		/// <param name="other">An object to compare with this object.</param>
		public bool Equals(CharMatch other)
		{
			return _kind == other._kind
				&& _category == other._category
				&& _primary == other._primary
				&& _opposing == other._opposing;
		}

		#endregion

		/// <summary>
		///   Creates an instance that matches any character except the char given and it's opposite case if it is a letter.
		/// </summary>
		/// <param name="c"></param>
		/// <returns></returns>
		public static CharMatch AnyCaseInsensitiveExcept(char c)
		{
			if (!char.IsLetter(c))
			{
				return AnyExcept(c);
			}
			else
			{
				return new CharMatch(CharMatchKind.NotCaseInsensitive, default(UnicodeCategory), c
														, char.IsUpper(c) ? char.ToLower(c) : char.ToUpper(c)
					);
			}
		}

		/// <summary>
		///   Creates an instance that matches any character that is not the exact character given.
		/// </summary>
		/// <param name="c"></param>
		/// <returns></returns>
		public static CharMatch AnyExcept(char c)
		{
			return new CharMatch(CharMatchKind.Not, default(UnicodeCategory), c, '\u0000');
		}

		/// <summary>
		///   Creates an instance that matches any character not in the given unicode category .
		/// </summary>
		/// <param name="cat">category</param>
		/// <returns></returns>
		public static CharMatch AnyUnicodeCategoryExcept(UnicodeCategory cat)
		{
			return new CharMatch(CharMatchKind.NotUnicodeCategory, cat, '\u0000', '\u0000');
		}

		/// <summary>
		///   Creates an instance that matches the character given or it's opposite case if it is a letter character.
		/// </summary>
		/// <param name="c"></param>
		/// <returns></returns>
		public static CharMatch CaseInsensitive(char c)
		{
			if (!char.IsLetter(c))
			{
				return Exact(c);
			}
			else if (char.IsLower(c))
			{
				return new CharMatch(CharMatchKind.CaseInsensitive, default(UnicodeCategory), c, char.ToUpper(c));
			}
			else if (char.IsUpper(c))
			{
				return new CharMatch(CharMatchKind.CaseInsensitive, default(UnicodeCategory), c, char.ToLower(c));
			}
			else
			{
				return Exact(c); // we get here on UnicodeCategories { LetterNumber, ModifierLetter, OtherLetter }			
			}
		}

		/// <summary>
		///   Creates an instance that matches the exact character given.
		/// </summary>
		/// <param name="c"></param>
		/// <returns></returns>
		public static CharMatch Exact(char c)
		{
			return new CharMatch(CharMatchKind.Exact, default(UnicodeCategory), c, '\u0000');
		}

		/// <summary>
		/// Creates a match rule for a range of characters.
		/// </summary>
		/// <param name="lower"></param>
		/// <param name="upper"></param>
		/// <returns></returns>
		public static CharMatch Range(char lower, char upper)
		{
			return new CharMatch(CharMatchKind.Range, default(UnicodeCategory), lower, upper);
		}

		/// <summary>
		///   Creates an instance that matches any character in the given unicode category .
		/// </summary>
		/// <param name="cat"></param>
		/// <returns></returns>
		public static CharMatch UnicodeCategory(UnicodeCategory cat)
		{
			return new CharMatch(CharMatchKind.UnicodeCategory, cat, '\u0000', '\u0000');
		}
	}
}